package de.bloxel.ui;

import static de.bloxel.core.BloxelFactory.DEFAULT_BLOXEL_DIMENSION;
import static de.bloxel.ui.BloxelActionController.ATTACH_ACTION;
import static de.bloxel.ui.BloxelActionController.DETACH_ACTION;
import static de.bloxel.ui.BloxelActionController.LOAD_ACTION;
import static de.bloxel.ui.BloxelActionController.MENU_ACTION;
import static de.bloxel.ui.BloxelActionController.SAVE_ACTION;
import static de.bloxel.ui.BloxelInventory.BLOXEL_TYPE_0;
import static de.bloxel.ui.BloxelInventory.BLOXEL_TYPE_1;
import static de.bloxel.ui.BloxelInventory.BLOXEL_TYPE_2;
import static de.bloxel.ui.BloxelInventory.BLOXEL_TYPE_3;
import static de.bloxel.ui.BloxelInventory.BLOXEL_TYPE_4;
import static de.bloxel.ui.BloxelInventory.BLOXEL_TYPE_5;
import static de.bloxel.ui.BloxelInventory.BLOXEL_TYPE_6;
import static de.bloxel.ui.BloxelInventory.BLOXEL_TYPE_7;
import static de.bloxel.ui.BloxelInventory.BLOXEL_TYPE_8;
import static de.bloxel.ui.BloxelInventory.BLOXEL_TYPE_9;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Sets;
import com.jme3.app.Application;
import com.jme3.app.StatsView;
import com.jme3.asset.plugins.ClasspathLocator;
import com.jme3.bounding.BoundingBox;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.collision.Collidable;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.FlyByCamera;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.PointLight;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.material.RenderState.FaceCullMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.niftygui.NiftyJmeDisplay;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.Spatial.CullHint;
import com.jme3.scene.shape.Dome;
import com.jme3.terrain.geomipmap.TerrainLodControl;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.terrain.heightmap.FaultHeightMap;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.WrapMode;
import com.jme3.water.SimpleWaterProcessor;

import de.bloxel.controller.ControllerMenu;
import de.bloxel.controller.HUDController;
import de.bloxel.controller.UIManager;
import de.bloxel.core.Bloxel;
import de.bloxel.core.BloxelFactory;
import de.bloxel.core.GameLoader;
import de.bloxel.core.events.Event;
import de.bloxel.core.events.EventListener;
import de.bloxel.core.events.EventLogger;
import de.bloxel.core.events.EventManager;
import de.bloxel.core.water.WaterFiller;
import de.bloxel.util.ColorRGBAUtils;
import de.bloxel.world.BloxelCache;
import de.bloxel.world.OpenWorldTerrainChunk;
import de.bloxel.world.TerrainChunk;
import de.bloxel.world.TerrainElement;

/**
 * @author klaus hauschild
 * @author patrick lorenz
 * @author stephan dorer
 * @author andreas.hoehmann@gmx.de
 * 
 * @since 0.0.0
 */
public class BloxelApplication extends Application implements EventListener {

  private static final Logger LOG = LoggerFactory.getLogger(BloxelApplication.class);

  final BloxelCache bloxelCache = new BloxelCache();

  private final Node rootNode = new Node("Root Node");
  private final Node guiNode = new Node("Gui Node");

  private final Vector3f walkDirection = new Vector3f();
  private final GameLoader gameLoader = new GameLoader(this);
  private UIManager uiManager;
  private BloxelActionController bloxelActionController;

  private final TerrainChunk terrainManager;
  private final Set<TerrainElement> currentTerrainElements = Sets.newHashSet();
  private Vector3f lastTerrainUpdatePosition;
  private BloxelInventory bloxelInventory;
  private BitmapFont guiFont;

  private Node bloxelsNode;
  private Node terrainDebugNode;
  private Geometry bloxelMouseMarker;
  private BulletAppState bulletAppState;
  private CharacterControl player;
  private BloxelFactory bloxelFactory;
  private FlyByCamera flyCam;
  private NiftyJmeDisplay niftyDisplay;
  private BitmapText fpsText;

  private BitmapText terrainElementsText;
  private float secondCounter;
  public final EventManager eventManager = new EventManager();

  private PointLight sunPointLight;
  private AmbientLight sunAmbientLight;
  private float sunLightAngel;
  private SimpleWaterProcessor waterProcessor;
  private Spatial waterPlane;
  private WaterFiller waterFillerListener;

  /**
   * Reichweite von der Playerposition aus, in der der Player Bloxel manipulieren kann. Gilt in alle Richtungen.
   */
  private int interactionRange = 5;

  public BloxelApplication(final BloxelApplicationSettingsProvider theBloxelApplicationSettingsProvider,
      final TerrainChunk theTerrainChunk) {
    setSettings(theBloxelApplicationSettingsProvider.getAppSettings());
    terrainManager = theTerrainChunk;
  }

  public void addBloxel(final Spatial spatial, final boolean addToPhysicalSpace) {
    if (addToPhysicalSpace) {
      getPhysicsSpace().add(spatial);
    }
    getBloxelsNode().attachChild(spatial);
  }

  private void addDemoTerraib() {
    float[] heightmap = new float[] {};
    try {
      heightmap = new FaultHeightMap(513, 50, FaultHeightMap.FAULTTYPE_COSINE, FaultHeightMap.FAULTSHAPE_CIRCLE, 0, 50,
          50).getHeightMap();
      // heightmap = new FluidSimHeightMap(513, 3000).getHeightMap();
      // heightmap = new HillHeightMap(513, 3000, 30, 60, (byte) 4).getHeightMap();
    } catch (final Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    final float grassScale = 64;
    final float dirtScale = 16;
    final float rockScale = 128;
    final Material matTerrain = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md");
    matTerrain.setBoolean("useTriPlanarMapping", false);
    matTerrain.setBoolean("WardIso", true);
    matTerrain.setTexture("AlphaMap", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));
    final Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");
    grass.setWrap(WrapMode.Repeat);
    matTerrain.setTexture("DiffuseMap", grass);
    matTerrain.setFloat("DiffuseMap_0_scale", grassScale);
    final Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");
    dirt.setWrap(WrapMode.Repeat);
    matTerrain.setTexture("DiffuseMap_1", dirt);
    matTerrain.setFloat("DiffuseMap_1_scale", dirtScale);
    final Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg");
    rock.setWrap(WrapMode.Repeat);
    matTerrain.setTexture("DiffuseMap_2", rock);
    matTerrain.setFloat("DiffuseMap_2_scale", rockScale);
    final TerrainQuad terrain = new TerrainQuad("Terrain", 65, 513, heightmap);
    final List<Camera> cameras = new ArrayList<Camera>();
    cameras.add(getCamera());
    final TerrainLodControl control = new TerrainLodControl(terrain, cameras);
    terrain.addControl(control);
    terrain.setMaterial(matTerrain);
    terrain.setModelBound(new BoundingBox());
    terrain.updateModelBound();
    terrain.setLocalScale(2f, 1f, 2f);
    terrain.setLocalTranslation(0, -200, 0);
    // -220 passt zu HillMap
    // terrain.setLocalTranslation(0, -220, 0);
    terrain.setLocked(true);
    final CollisionShape sceneShape = CollisionShapeFactory.createMeshShape(terrain);
    final RigidBodyControl landscape = new RigidBodyControl(sceneShape, 0);
    terrain.addControl(landscape);
    rootNode.attachChild(terrain);
    getPhysicsSpace().add(terrain);
  }

  public CollisionResult checkTargetPosition() {
    return checkTargetPosition(getBloxelsNode());
  }

  public CollisionResult checkTargetPosition(final Collidable aNode) {
    final Camera cam = getCamera();
    // Aim the ray from cam loc to cam direction.
    final Ray ray = new Ray(cam.getLocation(), cam.getDirection());
    return checkTargetPosition(aNode, ray);
  }

  private CollisionResult checkTargetPosition(final Collidable aNode, final Ray ray) {
    final CollisionResults results = new CollisionResults();
    // Collect intersections between Ray and Collidable in results list.
    aNode.collideWith(ray, results);
    if (results.size() == 0) {
      // no collision
      return null;
    }
    final CollisionResult closestCollision = results.getClosestCollision();
    if (interactionRange > 0
        && closestCollision.getDistance() > BloxelFactory.DEFAULT_BLOXEL_DIMENSION * 2 * interactionRange) {
      // collision is too far away
      return null;
    }
    // The closest collision point is what was truly hit:
    return closestCollision;
  }

  public Node getBloxelsNode() {
    return bloxelsNode;
  }

  public FlyByCamera getFlyCam() {
    return flyCam;
  }

  public NiftyJmeDisplay getNiftyDisplay() {
    return niftyDisplay;
  }

  PhysicsSpace getPhysicsSpace() {
    return bulletAppState.getPhysicsSpace();
  }

  public CharacterControl getPlayer() {
    return player;
  }

  public Node getRootNode() {
    return rootNode;
  }

  @Override
  public boolean handleEvent(final Event event) {
    return false;
  }

  private void initDebugView() {
    guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
    fpsText = new BitmapText(guiFont, false);
    fpsText.setLocalTranslation(0, fpsText.getLineHeight(), 0);
    guiNode.attachChild(fpsText);
    terrainElementsText = new BitmapText(guiFont, false);
    terrainElementsText.setLocalTranslation(80, terrainElementsText.getLineHeight(), 0);
    guiNode.attachChild(terrainElementsText);
    // move it up so it appears above fps text
    final StatsView statsView = new StatsView("Statistics View", assetManager, renderer.getStatistics());
    statsView.setLocalTranslation(0, fpsText.getLineHeight(), 0);
    guiNode.attachChild(statsView);
  }

  private void initFog() {
    // final FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
    // fpp.setNumSamples(4);
    // final FogFilter fog = new FogFilter();
    // fog.setFogColor(new ColorRGBA(0.9f, 0.9f, 0.9f, 1.0f));
    // fog.setFogDistance(64f);
    // fog.setFogDensity(1.2f);
    // fpp.addFilter(fog);
    // viewPort.addProcessor(fpp);
    // final BloomFilter bloom = new BloomFilter();
    // bloom.setBloomIntensity(0.8f);
    // bloom.setExposureCutOff(0.85f);
    // bloom.setExposurePower(0.8f);
    // bloom.setDownSamplingFactor(2.0f);
    // bloom.setBlurScale(4.0f);
    // fpp.addFilter(bloom);
    // viewPort.addProcessor(fpp);

  }

  @Override
  public void initialize() {

    // the following order is important!

    super.initialize();

    assetManager.registerLocator("/de/bloxel/Assert/", ClasspathLocator.class);

    guiNode.setQueueBucket(Bucket.Gui);
    guiNode.setCullHint(CullHint.Never);
    viewPort.attachScene(rootNode);
    guiViewPort.attachScene(guiNode);
    guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");

    bulletAppState = new BulletAppState();
    bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
    stateManager.attach(bulletAppState);

    niftyDisplay = new NiftyJmeDisplay(assetManager, inputManager, audioRenderer, guiViewPort);
    initNiftyGui();

    initWater();
    bloxelFactory = new BloxelFactory(assetManager, waterProcessor);
    bloxelInventory = new BloxelInventory(niftyDisplay.getNifty());
    bloxelActionController = new BloxelActionController(this, terrainManager, bloxelInventory);

    initPlayer();
    initKeys();
    initSkyBox();
    initTerrain();
    initWeather();
    initLights();
    initShadow();
    initFog();
    initSubSystems();

    // TODO statt die letzte szene zu laden muessen wir nun eigentlich nur noch die Map (das terrain laden/speichern)
    // a hack to load a initial scene :D
    // gameLoader.loadScene("scene.save");
    // eventManager.queueEvent(new LoadEvent("scene.save"));

    initDebugView();
  }

  private void initKeys() {
    // define mappings
    inputManager.addMapping(ATTACH_ACTION, new MouseButtonTrigger(0));
    inputManager.addMapping(DETACH_ACTION, new MouseButtonTrigger(1));
    inputManager.addMapping(MENU_ACTION, new KeyTrigger(KeyInput.KEY_ESCAPE));
    inputManager.addMapping(SAVE_ACTION, new KeyTrigger(KeyInput.KEY_F5));
    inputManager.addMapping(LOAD_ACTION, new KeyTrigger(KeyInput.KEY_F9));
    inputManager.addMapping("Lefts", new KeyTrigger(KeyInput.KEY_A));
    inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_D));
    inputManager.addMapping("Ups", new KeyTrigger(KeyInput.KEY_W));
    inputManager.addMapping("Downs", new KeyTrigger(KeyInput.KEY_S));
    inputManager.addMapping("Jumps", new KeyTrigger(KeyInput.KEY_SPACE));
    inputManager.addMapping(BLOXEL_TYPE_1, new KeyTrigger(KeyInput.KEY_1));
    inputManager.addMapping(BLOXEL_TYPE_2, new KeyTrigger(KeyInput.KEY_2));
    inputManager.addMapping(BLOXEL_TYPE_3, new KeyTrigger(KeyInput.KEY_3));
    inputManager.addMapping(BLOXEL_TYPE_4, new KeyTrigger(KeyInput.KEY_4));
    inputManager.addMapping(BLOXEL_TYPE_5, new KeyTrigger(KeyInput.KEY_5));
    inputManager.addMapping(BLOXEL_TYPE_6, new KeyTrigger(KeyInput.KEY_6));
    inputManager.addMapping(BLOXEL_TYPE_7, new KeyTrigger(KeyInput.KEY_7));
    inputManager.addMapping(BLOXEL_TYPE_8, new KeyTrigger(KeyInput.KEY_8));
    inputManager.addMapping(BLOXEL_TYPE_9, new KeyTrigger(KeyInput.KEY_9));
    inputManager.addMapping(BLOXEL_TYPE_0, new KeyTrigger(KeyInput.KEY_0));
    // attach to action-listeners
    inputManager.addListener(bloxelActionController, new String[] { SAVE_ACTION, LOAD_ACTION });
    inputManager.addListener(bloxelActionController, new String[] { ATTACH_ACTION, DETACH_ACTION, MENU_ACTION });
    inputManager.addListener(bloxelActionController, new String[] { "Lefts", "Rights", "Ups", "Downs", "Jumps" });
    inputManager.addListener(bloxelInventory, new String[] { BLOXEL_TYPE_1, BLOXEL_TYPE_2, BLOXEL_TYPE_3,
        BLOXEL_TYPE_4, BLOXEL_TYPE_5, BLOXEL_TYPE_6, BLOXEL_TYPE_7, BLOXEL_TYPE_8, BLOXEL_TYPE_9, BLOXEL_TYPE_0 });
  }

  private void initLights() {
    sunAmbientLight = new AmbientLight();
    sunAmbientLight.setColor(ColorRGBA.White.clone().mult(4f));
    rootNode.addLight(sunAmbientLight);
  }

  private void initNiftyGui() {
    getNiftyDisplay().getNifty().fromXml("de/bloxel/ui/nifty.xml", "start", new ControllerMenu(this),
        new HUDController(this));
    uiManager = new UIManager(getNiftyDisplay().getNifty());
    uiManager.initialize();
    eventManager.subscribeEvent("UIEvent", uiManager);
    // getNiftyDisplay().getNifty().fromXmlWithoutStartScreen("de/bloxel/ui/nifty.xml");
    // getNiftyDisplay().getNifty().registerScreenController(new ControllerMenu(this));
    // getNiftyDisplay().getNifty().registerScreenController(new HUDController());
    getGuiViewPort().addProcessor(getNiftyDisplay());
  }

  /**
   * Add a player ({@link CapsuleCollisionShape}, {@link CharacterControl}) to the world, create the {@link FlyByCamera}
   * .
   */
  private void initPlayer() {
    // We set up collision detection for the player by creating
    // a capsule collision shape and a physics character node.
    // The physics character node offers extra settings for
    // size, stepheight, jumping, falling, and gravity.
    // We also put the player in its starting position.
    final CapsuleCollisionShape capsuleShape = new CapsuleCollisionShape(1f, 2f, 1);
    capsuleShape.setMargin(1f);
    player = new CharacterControl(capsuleShape, 0.1f);
    player.setJumpSpeed(20);
    player.setFallSpeed(10);
    player.setPhysicsLocation(new Vector3f(0f, 3f, 0));
    getPhysicsSpace().add(player);
    player.setGravity(80);
    flyCam = new FlyByCamera(cam);
    flyCam.setMoveSpeed(20f);
    flyCam.registerWithInput(inputManager);
    cam.setFrustumFar(450);
    cam.setFrustumNear(0.5f);
  }

  private void initShadow() {
    // final BasicShadowRenderer bsRenderer = new BasicShadowRenderer(assetManager, 256);
    // bsRenderer.setDirection(lightDir.normalizeLocal());
    // viewPort.addProcessor(bsRenderer);
    // final PssmShadowRenderer pssmRenderer = new PssmShadowRenderer(assetManager, 1024, 1);
    // pssmRenderer.setDirection(new Vector3f(0.01f, -1f, 0.01f).normalizeLocal());
    // pssmRenderer.setLambda(0.55f);
    // pssmRenderer.setShadowIntensity(0.6f);
    // pssmRenderer.setCompareMode(CompareMode.Hardware);
    // pssmRenderer.setFilterMode(FilterMode.PCF4);
    // viewPort.addProcessor(pssmRenderer);
  }

  private void initSkyBox() {
    final Dome dome = new Dome(Vector3f.ZERO, 10, 10, 10, false);
    final Geometry sky = new Geometry("Sky", dome);
    sky.setQueueBucket(Bucket.Sky);
    sky.setCullHint(Spatial.CullHint.Never);
    sky.setLocalScale(512f);
    // sky.setLocalTranslation(0, -100, 0);
    final Material material = new Material(assetManager, "Common/MatDefs/Misc/SolidColor.j3md");
    material.setColor("Color", ColorRGBAUtils.hexToRGBA("00BFFF"));
    material.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off);
    sky.setMaterial(material);
    rootNode.attachChild(sky);
  }

  private void initSubSystems() {
    eventManager.subscribeEvent("ALL", new EventLogger());
    eventManager.subscribeEvent("SAVE", gameLoader);
    eventManager.subscribeEvent("LOAD", gameLoader);
    waterFillerListener = new WaterFiller(this, bloxelActionController);
    eventManager.subscribeEvent("waterSourceCreate", waterFillerListener);
  }

  private void initTerrain() {
    bloxelsNode = new Node("Bloxels");
    rootNode.attachChild(bloxelsNode);
    terrainDebugNode = new Node("BloxelsDebug");
    rootNode.attachChild(terrainDebugNode);
    // addDemoTerraib();
    updateTerrain(Vector3f.ZERO, Vector3f.ZERO);
  }

  private void initWater() {
    waterProcessor = new SimpleWaterProcessor(assetManager);
    waterProcessor.setWaterColor(ColorRGBA.Blue);
    waterProcessor.setWaterTransparency(0.4f);
    viewPort.addProcessor(waterProcessor);
    // XXX scheinbar berechnet der processor eine art preview von der ReflectionScene, diese scene sollte auch KEINEN
    // wasserblock enthalten ... waere cool wenn wir nach jedem bloxel der in der welt dazukommt, die wasser texturen
    // aktuallisieren: alle wasserbloecke finden, den wasser process neu bauen (die node aus der der sich bedient,
    // sollte dann alle bloxel enthalten aber keine wssser bloxel) und die texture der wasserbloxel austauschen
    waterProcessor.setReflectionScene(rootNode);
  }

  private void initWeather() {
    // rootNode.attachChild(new Rain(assetManager, cam, 1));
    // final Material cloudMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
    // final Texture clouds = assetManager.loadTexture("Textures/Maps/clouds.png");
    // cloudMat.setTexture("m_DiffuseMap", clouds);
    // cloudMat.setBoolean("m_UseAlpha", true);
    // cloudMat.setBoolean("m_UseMaterialColors", true);
    // cloudMat.setColor("m_Ambient", ColorRGBA.Black);
    // cloudMat.setColor("m_Diffuse", ColorRGBA.White);
    // cloudMat.setColor("m_Specular", ColorRGBA.White);
    // cloudMat.setFloat("m_Shininess", 2);
    // cloudMat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
    // final Spatial cloudSphereG = new Geometry("Clouds", new Sphere(20, 20, 400));
    // cloudSphereG.setQueueBucket(Bucket.Transparent);
  }

  private void markBloxelMouse() {
    final CollisionResult cr = checkTargetPosition(getBloxelsNode());
    if (bloxelMouseMarker != null) {
      rootNode.detachChild(bloxelMouseMarker);
    }
    if (cr != null) {
      if (bloxelMouseMarker != null) {
        rootNode.detachChild(bloxelMouseMarker);
      }
      if (cr.getGeometry() != null) {
        bloxelMouseMarker = cr.getGeometry().clone(false);
        final Material material = new Material(assetManager, "Common/MatDefs/Misc/SolidColor.j3md");
        material.setColor("Color", new ColorRGBA(1, 1, 1, 0.1f));
        material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
        final Material matWire = new Material(assetManager, "Common/MatDefs/Misc/WireColor.j3md");
        matWire.setColor("Color", ColorRGBA.Red);
        bloxelMouseMarker.setMaterial(matWire);
        bloxelMouseMarker.scale(1.1f);
        rootNode.attachChild(bloxelMouseMarker);
        return;
      }
    } else {
      if (bloxelMouseMarker != null) {
        rootNode.detachChild(bloxelMouseMarker);
      }
    }
  }

  private void movePlayer() {
    final Vector3f camDir = cam.getDirection().clone().multLocal(.15f);
    final Vector3f camLeft = cam.getLeft().clone().multLocal(.15f);
    walkDirection.set(0, 0, 0);
    final float speedUp = 1.5f;
    if (bloxelActionController.isLeft()) {
      walkDirection.addLocal(camLeft.mult(speedUp));
    }
    if (bloxelActionController.isRight()) {
      walkDirection.addLocal(camLeft.negate().mult(speedUp));
    }
    if (bloxelActionController.isUp()) {
      walkDirection.addLocal(camDir.mult(speedUp));
    }
    if (bloxelActionController.isDown()) {
      walkDirection.addLocal(camDir.negate().mult(speedUp));
    }
    player.setWalkDirection(walkDirection);
    cam.setLocation(player.getPhysicsLocation());
    final float distance = player.getPhysicsLocation().distance(lastTerrainUpdatePosition);
    // System.out.println("lastTerrainUpdatePosition" + lastTerrainUpdatePosition);
    // System.out.println("Direction" + walkDirection);
    // System.out.println("Location" + player.getPhysicsLocation());
    // System.out.println("Run " + distance + " since last terrain update");
    if (distance > DEFAULT_BLOXEL_DIMENSION) {
      LOG.trace(String.format("Distance since last Terrain Update to big - update terrain : %f", distance));
      updateTerrain(player.getPhysicsLocation(), player.getWalkDirection());
    }
  }

  public void setInteractionRange(final int theInteractionRange) {
    interactionRange = theInteractionRange;
  }

  /**
   * Simple update and root node.
   * 
   * @param tpf
   */
  private void simpleUpdate(final float tpf) {
    movePlayer();
    markBloxelMouse();
    if (niftyDisplay.getNifty().isActive("de/bloxel/ui/nifty.xml", "menu")) {
      flyCam.setEnabled(false);
    } else {
      flyCam.setEnabled(true);
      flyCam.setDragToRotate(false);
    }

  }

  @Override
  public void update() {
    super.update(); // makes sure to execute AppTasks
    if (speed == 0 || paused) {
      return;
    }
    final float tpf = timer.getTimePerFrame() * speed;

    updateDebugView();

    // update states
    stateManager.update(tpf);

    // simple update and root node
    simpleUpdate(tpf);

    eventManager.doEvents();

    // simple update and root node
    rootNode.updateLogicalState(tpf);
    guiNode.updateLogicalState(tpf);
    rootNode.updateGeometricState();
    guiNode.updateGeometricState();

    // render states
    stateManager.render(renderManager);
    renderManager.render(tpf);
    stateManager.postRender();
  }

  private void updateDebugView() {
    secondCounter += timer.getTimePerFrame();
    final int fps = (int) timer.getFrameRate();
    if (secondCounter >= 1.0f) {
      fpsText.setText(String.format("%d fps", fps));
      secondCounter = 0.0f;
    }
    terrainElementsText.setText(String.format("te: %d/%d/%d", terrainManager.getNumberOfElements(),
        currentTerrainElements.size(), bloxelCache.getSize()));
  }

  public void updateTerrain() {
    updateTerrain(player.getPhysicsLocation(), player.getWalkDirection());
  }

  /**
   * Load the "visible" {@link TerrainElement}S from loaded Terrain around the player and update the
   * <code>bloxelsNode</code>.
   * 
   * @param vector3f
   */
  public void updateTerrain(final Vector3f newPlayerPosition, final Vector3f walkDirection) {
    long start = System.currentTimeMillis();
    final Set<TerrainElement> terrainElements = Sets.newHashSet(terrainManager.getTerrainElements(newPlayerPosition,
        walkDirection));
    final Set<TerrainElement> currentTe = Sets.newLinkedHashSet(currentTerrainElements);
    final Set<TerrainElement> elementsToAdd = Sets.difference(terrainElements, currentTe);
    final Set<TerrainElement> elementsToRemove = Sets.difference(currentTe, terrainElements);
    LOG.debug("sort time: {}", (System.currentTimeMillis() - start));
    LOG.trace(String.format("To Add: %d", elementsToAdd.size()));
    LOG.trace(String.format("To Remove: %d", elementsToRemove.size()));
    start = System.currentTimeMillis();
    for (final TerrainElement te : elementsToRemove) {
      final Bloxel bloxel = bloxelCache.getFromCache(te.getCenter());
      if (bloxel == null) {
        LOG.warn("Can't remove te '%s' from world!", te);
        continue;
      }
      if (getBloxelsNode().detachChild(bloxel) == -1) {
        LOG.warn("Can't remove bloxel '%s' from scene graph!", bloxel);
      }
      getPhysicsSpace().remove(bloxel);
      bloxelCache.remove(te.getCenter());
      currentTerrainElements.remove(te);
    }
    LOG.debug("remove time: {}", (System.currentTimeMillis() - start));
    start = System.currentTimeMillis();
    for (final TerrainElement te : elementsToAdd) {
      Bloxel bloxel = bloxelCache.getFromCache(te.getCenter());
      if (bloxel == null) {
        LOG.debug("does not contain bloxel. must create new one");
        bloxel = bloxelFactory.create(te.getBloxelType());
        bloxel.setLocalTranslation(te.getCenter());
        bloxelCache.putInCache(bloxel);
        currentTerrainElements.add(te);
      }
      addBloxel(bloxel, te.isInPhysicalSpacePresent());
    }
    LOG.debug("add time: {}", (System.currentTimeMillis() - start));
    lastTerrainUpdatePosition = newPlayerPosition.clone();
    if (terrainManager instanceof OpenWorldTerrainChunk) {
      // debug terrain chunks
      ((OpenWorldTerrainChunk) terrainManager).debug(terrainDebugNode, assetManager);
    }
  }
}